//!
//! # Tendermint ABCI workflow
//!

#![allow(warnings)]

use crate::ledger::State;
use crate::{
    cfg::DaemonCfg as Cfg,
    common::{BlockHeight, HashValue},
    ethvm::DzkAccount,
    ledger::{staking::Punishment, Ledger, Receipt},
    tx::Tx,
    InitialState,
};
use abci::Application;
use ethereum_types::{H160, U256};
use ruc::*;
use std::{collections::BTreeMap, fmt::format};
use tendermint::PublicKey as TmPubKey;
use tmtypes::{
    abci::{
        RequestBeginBlock, RequestCheckTx, RequestDeliverTx, RequestEndBlock,
        RequestInfo, RequestInitChain, ResponseBeginBlock, ResponseCheckTx,
        ResponseCommit, ResponseDeliverTx, ResponseEndBlock, ResponseInfo,
        ResponseInitChain, ValidatorUpdate,
    },
    crypto::PublicKey as TmPubKeyRaw,
};
use vsdb::{MapxOrd, VsMgmt};

#[derive(Clone)]
pub struct App {
    pub cfg: Cfg,
    pub ledger: Ledger,
}

impl App {
    fn new(cfg: Cfg) -> Result<Self> {
        Ledger::new(&cfg).c(d!()).map(|ledger| Self { cfg, ledger })
    }

    pub fn load_or_create(cfg: Cfg) -> Result<Self> {
        cfg.set_vsdb_base_dir().c(d!())?;
        if let Some(ledger) = Ledger::load_from_snapshot(&cfg).c(d!())? {
            Ok(Self { cfg, ledger })
        } else {
            Self::new(cfg).c(d!())
        }
    }

    #[inline(always)]
    #[cfg(target_os = "linux")]
    fn btm_snapshot(&self, height: BlockHeight) -> Result<()> {
        if self.cfg.snap_enable {
            self.cfg.snapshot(height).c(d!())
        } else {
            Ok(())
        }
    }

    #[inline(always)]
    #[cfg(not(target_os = "linux"))]
    fn btm_snapshot(&self, _: BlockHeight) -> Result<()> {
        Ok(())
    }
}

impl Application for App {
    fn init_chain(&self, req: RequestInitChain) -> ResponseInitChain {
        if !req.app_state_bytes.is_empty() {
            let distributions =
                pnk!(serde_json::from_slice::<InitialState>(&req.app_state_bytes))
                    .addr_to_amount;
            let l = self.ledger.main.write();
            for (addr, am) in distributions.into_iter() {
                let acc = DzkAccount::from_balance(am);
                pnk!(l.state.evm.token.accounts.insert(addr, acc));
            }
        }

        ResponseInitChain::default()
    }

    fn begin_block(&self, mut req: RequestBeginBlock) -> ResponseBeginBlock {
        let header = req.header.unwrap();
        let height = header.height as u64;
        let ts = header.time.unwrap().seconds as u64;

        pnk!(self.ledger.consensus_refresh(header.proposer_address, ts));

        info_omit!(self.btm_snapshot(height));

        let governances = {
            let mut online = vec![];
            let mut offline = vec![];
            let mut malicious = vec![];

            if let Some(lci) = req.last_commit_info.take() {
                for votes in lci.votes.into_iter() {
                    if votes.signed_last_block {
                        if let Some(v) = votes.validator {
                            online.push(v.address);
                        }
                    } else {
                        if let Some(v) = votes.validator {
                            offline.push(v.address);
                        }
                    }
                }
            }

            for m in req.byzantine_validators.into_iter() {
                if let Some(v) = m.validator {
                    malicious.push(v.address);
                }
            }

            vec![
                Punishment::Malicious(malicious),
                Punishment::Offline((offline, online)),
            ]
        };

        pnk!(
            self.ledger
                .main
                .write()
                .state
                .staking
                .governance_with_each_block(governances)
        );

        ResponseBeginBlock::default()
    }

    fn deliver_tx(&self, req: RequestDeliverTx) -> ResponseDeliverTx {
        let mut resp = ResponseDeliverTx::default();

        if let Ok(tx) = Tx::deserialize(&req.tx) {
            if tx.valid_in_abci() {
                let mut sb = self.ledger.deliver_tx.write();
                if let Err(e) = info!(sb.apply_tx(tx.clone())) {
                    resp.log = e.to_string();
                    resp.code = 1;
                }
            } else {
                resp.log = "Should not appear in ABCI".to_owned();
                resp.code = 1;
            }
        } else {
            resp.log = "Invalid data format".to_owned();
            resp.code = 1;
        }

        resp
    }

    fn end_block(&self, req: RequestEndBlock) -> ResponseEndBlock {
        let mut resp = ResponseEndBlock::default();
        if 15 == req.height % 16 {
            let l = self.ledger.main.write();
            resp.validator_updates = l
                .state
                .staking
                .validator_power_top_100()
                .into_iter()
                .map(|(pk, power)| ValidatorUpdate {
                    pub_key: TmPubKey::from_raw_ed25519(&pk).map(|pk| pk.into()),
                    power,
                })
                .collect();
        }
        resp
    }

    fn commit(&self) -> ResponseCommit {
        pnk!(self.ledger.commit());

        let mut r = ResponseCommit::default();
        r.data = self.ledger.main.read().last_block_hash();
        r
    }

    fn info(&self, req: RequestInfo) -> ResponseInfo {
        let mut resp = ResponseInfo::default();

        let ledger = self.ledger.main.read();

        let b = ledger.last_block().unwrap_or_default();
        let h = b.header.height as i64;

        resp.last_block_height = h;
        if 0 < h {
            resp.last_block_app_hash = b.header_hash;
        }

        println!("\n\n");
        println!("==========================================");
        println!("======== Last committed height: {} ========", h);
        println!("==========================================");
        println!("\n\n");

        resp
    }

    fn check_tx(&self, req: RequestCheckTx) -> ResponseCheckTx {
        let mut resp = ResponseCheckTx::default();
        alt!(0 != req.r#type, return resp);

        match Tx::deserialize(&req.tx) {
            Ok(_tx) => {
                // if tx.valid_in_abci() {
                //     let mut sb = self.ledger.check_tx.write();
                //     if let Err(e) = info!(sb.apply_tx(tx)) {
                //         resp.log = e.to_string();
                //         resp.code = 1;
                //     }
                // } else {
                //     resp.log = "Should not appear in ABCI".to_owned();
                //     resp.code = 1;
                // }
            }
            Err(e) => {
                println!("deserialize err:{:?}", e);
                resp.log = e.to_string();
                resp.code = 1;
            }
        }

        resp
    }
}
